#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include <elf.h>

static char *file_start;

#pragma pack(1)

typedef signed char s8;
typedef signed short s16;
typedef signed int s32;
typedef signed long long s64;
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef unsigned long long u64;


#define swap32(x) ({\
	unsigned _x = (x); \
	(((_x)>>24)&0xff)\
	|\
	(((_x)>>8)&0xff00)\
	|\
	(((_x)<<8)&0xff0000)\
	|\
	(((_x)<<24)&0xff000000);\
})

#define swap16(x) ({unsigned _x = (x); (((_x)>>8)&0xff) | (((_x)<<8)&0xff00); })

#if defined(__BIG_ENDIAN__) || defined(_BIG_ENDIAN)

#define le32_to_cpu(x) swap32(x)
#define le16_to_cpu(x) swap16(x)
#define be32_to_cpu(x) (x)
#define be16_to_cpu(x) (x)

#else

#define le32_to_cpu(x) (x)
#define le16_to_cpu(x) (x)
#define be32_to_cpu(x) swap32(x)
#define be16_to_cpu(x) swap16(x)

#endif

#define cpu_to_le32(x) le32_to_cpu(x)
#define cpu_to_le16(x) le16_to_cpu(x)
#define cpu_to_be32(x) be32_to_cpu(x)
#define cpu_to_be16(x) be16_to_cpu(x)

/////////////////////////////////////

static unsigned crctable[256];

#define POLYNOMIAL 0x04C11DB7

static void init_crc(void){
	unsigned dividend = 0;
	do{
		unsigned remainder = dividend << 24;
		unsigned bit = 0;
		do{
			if(remainder & 0x80000000){
				remainder <<= 1;
				remainder ^= POLYNOMIAL;
			}else{
				remainder <<= 1;
			}
		}while(++bit<8);
		crctable[dividend++] = remainder;
	}while(dividend<256);
}

// for Marvell, pass 0 as the initial remainder
static unsigned do_crc(unsigned remainder, const unsigned char *p, unsigned n){
	unsigned i = 0;
	while(i < n){
		unsigned char data = *p ^ (remainder >> 24);
		remainder = crctable[data] ^ (remainder << 8);
		p++;
		i++;
	}
	return remainder;
}

///////////////////////////////////////

#define need(size,msg)({        \
	if(mapsize<(int)(size)){   \
		fprintf(stderr,"not enough bytes, have %d of %d for %s\n",(int)mapsize,(int)(size),msg);     \
		return;     \
	}                    \
})

////////////////////////////////////

/** fwheader */
struct fwheader {
	u32 dnldcmd;    // 1, except 4 for the 0-sized block at the end
	u32 baseaddr;   // within 64 k following 0 or 0xc0000000
	u32 datalength; // The driver accepts 0 to 600. We see 512, plus short blocks at the end of a segment/section.
	u32 CRC;
};
// for sending to the chip, an extra u32 seq num goes here (before data)

#define FW_HAS_DATA_TO_RECV		0x00000001
#define FW_HAS_LAST_BLOCK		0x00000004

////////////////////////////////////

#define HALF_NUM_BLOCKS ((64*1024+508)/508) /* max num blocks for 64 K */

static char bindata[2*HALF_NUM_BLOCKS*(16+508+4)+16];
static char *pastdata = bindata;

static unsigned char *seg0;
static unsigned char *segc;
static unsigned seg0sz;
static unsigned segcsz;
static unsigned c_start;

#define ARRAYSIZE(x) (sizeof(x)/sizeof((x)[0]))

static void spew(unsigned char *p, unsigned n, unsigned addr){
	if(!p || !n)
		return;
	char *where = pastdata;
	for(;;){
		unsigned amt = n;
		if(amt>508)
			amt = 508;
		if(!amt)
			break;
		struct fwheader fwheader;
		fwheader.dnldcmd = cpu_to_le32(FW_HAS_DATA_TO_RECV);
		fwheader.baseaddr = cpu_to_le32(addr);
		fwheader.datalength = cpu_to_le32(amt+4);
		fwheader.CRC = 0;
		fwheader.CRC = cpu_to_be32(do_crc(0,(unsigned char*)&fwheader,12));
		memcpy(where,&fwheader,16);
		where += 16;
		memcpy(where,p,amt);
		// we rely on static zero initialization of 4 bytes on the end
		unsigned crc = cpu_to_be32(do_crc(0,(unsigned char*)where,amt));
		where += amt;
		p += amt;
		n -= amt;
		addr += amt;
		memcpy(where,&crc,4);
		where += 4;
	}
	pastdata = where;
}

static unsigned entrypoint;

static int mkbin(void){
	if(segcsz&3 || seg0sz&3){
		fprintf(stderr,"data size causes misalignment 0x%08x 0x%08x\n",segcsz,seg0sz);
		exit(45);
	}
	spew(segc,segcsz,c_start);
	spew(seg0,seg0sz,0x00000000u);
	struct fwheader fwheader = {cpu_to_le32(FW_HAS_LAST_BLOCK),entrypoint,0,0};
	fwheader.CRC = cpu_to_be32(do_crc(0,(unsigned char*)&fwheader,12));
	memcpy(pastdata,&fwheader,16);
	pastdata += 16;
	return pastdata-bindata;
}


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

static void parse_elf(char *map, ssize_t mapsize){
	int i;
	Elf32_Ehdr ehdr;
	need(sizeof ehdr,"ELF header");
	memcpy(&ehdr,map,sizeof ehdr);

	if(map[0]!='\177' || map[1]!='E' || map[2]!='L' || map[3]!='F' || map[4]!=ELFCLASS32 || map[5]!=ELFDATA2LSB || map[6]!=EV_CURRENT){
		fprintf(stderr,"no ELF magic\n");
		exit(84);
	}
#define X16(x) (ehdr.x = le16_to_cpu(ehdr.x))
#define X32(x) (ehdr.x = le32_to_cpu(ehdr.x))
	X16(e_type); X16(e_machine); X32(e_version); X32(e_entry); X32(e_phoff); X32(e_shoff);
	X32(e_flags); X16(e_ehsize); X16(e_phentsize); X16(e_phnum); X16(e_shentsize);
	X16(e_shnum); X16(e_shstrndx);
#undef X16
#undef X32
	if(ehdr.e_machine != EM_ARM){
		fprintf(stderr,"arch 0x%04hx is not ARM\n",ehdr.e_machine);
		exit(44);
	}
	if(ehdr.e_version != EV_CURRENT){
		fprintf(stderr,"ver 0x%08x is not current\n",ehdr.e_version);
		exit(50);
	}
	entrypoint = ehdr.e_entry;
	if(ehdr.e_ehsize != sizeof ehdr || ehdr.e_phentsize != sizeof(Elf32_Phdr) || ehdr.e_shentsize != sizeof(Elf32_Shdr)){
		fprintf(stderr,"bad header size\n");
		exit(52);
	}
	if(ehdr.e_phnum<2 && ehdr.e_shnum<2){
		fprintf(stderr,"forget your data?\n");
		exit(48);
	}
	if(ehdr.e_phoff || ehdr.e_phnum){
		if(ehdr.e_phoff < sizeof ehdr || ehdr.e_phoff >= (unsigned)mapsize || ehdr.e_phnum > (mapsize-ehdr.e_phoff)/sizeof(Elf32_Phdr)){
			fprintf(stderr,"program headers don't fit right\n");
			exit(14);
		}
	}
	if(ehdr.e_shoff || ehdr.e_shnum){
		if(ehdr.e_shoff < sizeof ehdr || ehdr.e_shoff >= (unsigned)mapsize || ehdr.e_shnum > (mapsize-ehdr.e_shoff)/sizeof(Elf32_Shdr)){
			fprintf(stderr,"section headers don't fit right\n");
			exit(15);
		}
	}
	/////// Look in the program headers
	unsigned phdr_seg0sz = 0;
	unsigned phdr_seg0 = 0;
	unsigned phdr_segcsz = 0;
	unsigned phdr_segc = 0;
	unsigned phdr_c_count = 0;
	unsigned phdr_c_start = 0;
	i = ehdr.e_phnum;
	while(i--){
		Elf32_Phdr phdr;
		memcpy(&phdr, map+ehdr.e_phoff+i*sizeof(Elf32_Phdr), sizeof(Elf32_Phdr));
#define X16(x) (phdr.x = le16_to_cpu(phdr.x))
#define X32(x) (phdr.x = le32_to_cpu(phdr.x))
		X32(p_type); X32(p_offset); X32(p_vaddr); X32(p_paddr); X32(p_filesz); X32(p_memsz); X32(p_flags); X32(p_align);
#undef X16
#undef X32
//		fprintf(stderr,"phdr %08x %08x %08x %08x %08x %08x %08x %08x\n",phdr.p_type, phdr.p_offset, phdr.p_vaddr, phdr.p_paddr, phdr.p_filesz,phdr.p_memsz, phdr.p_flags, phdr.p_align);
		if(phdr.p_type==PT_NULL || phdr.p_type==PT_NOTE)
			continue;
		if(phdr.p_type!=PT_LOAD){
			fprintf(stderr,"bad phdr.p_type 0x%08x\n",phdr.p_type);
			exit(53);
		}
		if(phdr.p_offset<sizeof ehdr
		|| phdr.p_offset>=(unsigned)mapsize
		|| phdr.p_filesz>=(unsigned)mapsize
		|| phdr.p_memsz>=(unsigned)mapsize
		|| phdr.p_filesz+phdr.p_offset>=(unsigned)mapsize
		|| phdr.p_memsz<phdr.p_filesz
		|| phdr.p_memsz>0x00028000u
		|| phdr.p_filesz>0x00028000u){
			fprintf(stderr,"p_offset 0x%08x p_filesz 0x%08x p_memsz 0x%08x\n",
				phdr.p_offset,phdr.p_filesz,phdr.p_memsz);
			exit(16);
		}
		if(phdr.p_align>0x00010000u){
			fprintf(stderr,"p_align 0x%08x\n",phdr.p_align);
			exit(17);
		}
		if(phdr.p_flags != (PF_R|PF_X) && phdr.p_flags != (PF_R|PF_W) && phdr.p_flags != (PF_R|PF_W|PF_X)){
			fprintf(stderr,"p_flags 0x%08x\n",phdr.p_flags);
			exit(18);
		}
		if(phdr.p_paddr && phdr.p_paddr!=phdr.p_vaddr){
			fprintf(stderr,"addr %08x != %08x\n",phdr.p_paddr,phdr.p_vaddr);
			exit(20);
		}
		if(phdr.p_vaddr && (phdr.p_vaddr<0xc0000000u || phdr.p_vaddr>0xc0027fffu)){
			fprintf(stderr,"addr 0x%08x unknown\n",phdr.p_vaddr);
			exit(54);
		}
		switch(phdr.p_vaddr){
		default:
			fprintf(stderr,"addr 0x%08x unknown\n",phdr.p_vaddr);
			exit(21);
		case 0x00000000u:
			if(phdr.p_memsz>0x10000 || phdr.p_flags!=(PF_R|PF_X) || phdr.p_filesz<4){
				fprintf(stderr,"bad phdr for 0x00000000\n");
				exit(19);
			}
			phdr_seg0 = phdr.p_offset;
			phdr_seg0sz = phdr.p_filesz;
			break;
		case 0x04000000u:
			if(phdr.p_memsz>0x2000 || phdr.p_flags!=(PF_R|PF_W) || phdr.p_filesz){
				fprintf(stderr,"bad phdr for 0x04000000\n");
				exit(24);
			}
			break;
		case 0x80000000u:
			if(phdr.p_memsz>0x10000 || phdr.p_flags!=(PF_R|PF_W) || phdr.p_filesz){
				fprintf(stderr,"bad phdr for 0x80000000\n");
				exit(25);
			}
			break;
		case 0x90000000u:
			if(phdr.p_memsz>0x10000 || phdr.p_flags!=(PF_R|PF_W) || phdr.p_filesz){
				fprintf(stderr,"bad phdr for 0x90000000\n");
				exit(55);
			}
			break;
		case 0xc0000000u ... 0xc0027fffu:
			if(phdr.p_memsz>=0x28000 || phdr.p_vaddr+phdr.p_memsz>=0xc0028000 || (phdr.p_flags&PF_R)!=PF_R){
				fprintf(stderr,"bad phdr for 0x%08x\n", phdr.p_vaddr);
				exit(22);
			}
			if(phdr.p_filesz){
				phdr_segc = phdr.p_offset;
				phdr_segcsz = phdr.p_filesz;
				phdr_c_count++;
				phdr_c_start = phdr.p_vaddr;
			}
			break;
		case 0xfffe0000u ... 0xffff0000u:
			if(phdr.p_memsz>0x20000 || phdr.p_flags!=(PF_R|PF_X) || phdr.p_filesz){
				fprintf(stderr,"bad phdr for ROM area\n");
				exit(56);
			}
			break;
		}
	}
	/////// Look in the section headers
	unsigned shdr_seg0sz = 0;
	unsigned shdr_seg0 = 0;
	unsigned shdr_segcsz = 0;
	unsigned shdr_segc = 0;
	unsigned shdr_c_count = 0;
	unsigned shdr_c_start = 0;
	i = ehdr.e_shnum;
	while(i--){
		Elf32_Shdr shdr;
		memcpy(&shdr, map+ehdr.e_shoff+i*sizeof(Elf32_Shdr), sizeof(Elf32_Shdr));
#define X16(x) (shdr.x = le16_to_cpu(shdr.x))
#define X32(x) (shdr.x = le32_to_cpu(shdr.x))
		X32(sh_name); X32(sh_type); X32(sh_flags); X32(sh_addr); X32(sh_offset);
		X32(sh_size); X32(sh_link); X32(sh_info); X32(sh_addralign); X32(sh_entsize);
#undef X16
#undef X32
//		fprintf(stderr,"shdr %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n",
//			shdr.sh_name, shdr.sh_type, shdr.sh_flags, shdr.sh_addr, shdr.sh_offset,
//			shdr.sh_size, shdr.sh_link, shdr.sh_info, shdr.sh_addralign, shdr.sh_entsize
//		);
		switch(shdr.sh_type){
		default:
			continue;
		case SHT_RELA:
		case SHT_REL:
			fprintf(stderr,"can not relocate!\n");
			exit(26);
		case SHT_DYNAMIC:
		case SHT_SHLIB:
		case SHT_DYNSYM:
			fprintf(stderr,"can not dynamicly link!\n");
			exit(27);
		case SHT_PROGBITS:
		case SHT_NOBITS:
			break;
		}
		if(shdr.sh_entsize || shdr.sh_info || shdr.sh_link || shdr.sh_addralign>0x10000){
			fprintf(stderr,"bad shdr stuff\n");
			exit(28);
		}
		if(shdr.sh_offset > (unsigned)mapsize){
			fprintf(stderr,"bad sh_offset 0x%08x\n",shdr.sh_offset);
			exit(38);
		}
		switch(shdr.sh_addr){
		default:
			fprintf(stderr,"addr 0x%08x unknown\n",shdr.sh_addr);
			exit(29);
		case 0x00000000u:
			if(!(shdr.sh_flags&SHF_ALLOC))
				break; /* probably debug info and similar */
			if(shdr.sh_type!=SHT_PROGBITS || shdr.sh_size>0x10000 || shdr.sh_size<4){
				fprintf(stderr,"bad shdr for 0x00000000\n");
				exit(30);
			}
			if((shdr.sh_flags&(SHF_ALLOC|SHF_EXECINSTR))!=(SHF_ALLOC|SHF_EXECINSTR)){
				fprintf(
					stderr,
					"bad shdr for 0x00000000 SHF_ALLOC=%d SHF_EXECINSTR=%d SHF_WRITE=%d\n",
					!!(shdr.sh_flags&SHF_ALLOC),
					!!(shdr.sh_flags&SHF_EXECINSTR),
					!!(shdr.sh_flags&SHF_WRITE)
				);
				exit(34);
			}
			if(shdr.sh_size + shdr.sh_offset > (unsigned)mapsize){
				fprintf(stderr,"bad shdr for 0x00000000\n");
				exit(37);
			}
			shdr_seg0 = shdr.sh_offset;
			shdr_seg0sz = shdr.sh_size;
			break;
		case 0x04000000u:
			if(shdr.sh_type!=SHT_NOBITS || shdr.sh_flags!=(SHF_ALLOC|SHF_WRITE) || shdr.sh_size!=0x2000){
				fprintf(stderr,"bad shdr for 0x04000000\n");
				exit(31);
			}
			break;
		case 0x80000000u:
			if(shdr.sh_type!=SHT_NOBITS || shdr.sh_size>0x10000 || shdr.sh_flags!=(SHF_ALLOC|SHF_WRITE)){
				fprintf(stderr,"bad shdr for 0x80000000\n");
				exit(32);
			}
			break;
		case 0x90000000u:
			if(shdr.sh_type!=SHT_NOBITS || shdr.sh_size>0x10000 || shdr.sh_flags!=(SHF_ALLOC|SHF_WRITE)){
				fprintf(stderr,"bad shdr for 0x90000000\n");
				exit(33);
			}
			break;
		case 0xc0000000u ... 0xc0027fffu:
			if(shdr.sh_size>0x28000 || shdr.sh_addr+shdr.sh_size>0xc0028000u){
				fprintf(stderr,"bad shdr for 0xc00??000\n");
				exit(35);
			}
			if(shdr.sh_type==SHT_NOBITS && shdr.sh_flags==(SHF_ALLOC|SHF_WRITE))
				break;
			if(shdr.sh_type!=SHT_PROGBITS || (shdr.sh_flags&SHF_ALLOC)!=SHF_ALLOC){
				fprintf(stderr,"bad shdr for 0xc00??000\n");
				exit(34);
			}
			if(shdr.sh_size){
				shdr_segc = shdr.sh_offset;
				shdr_segcsz = shdr.sh_size;
				shdr_c_count++;
				shdr_c_start = shdr.sh_addr;
			}
			break;
		case 0xfffe0000u ... 0xffff0000u:
			if(shdr.sh_type!=SHT_NOBITS || shdr.sh_size>0x20000 || shdr.sh_flags!=(SHF_ALLOC|SHF_EXECINSTR)){
				fprintf(stderr,"bad shdr for ROM area\n");
				exit(36);
			}
			break;
		}
	}
	////// If we got both types of header, ensure they match
	if(shdr_seg0sz && phdr_seg0sz && shdr_seg0sz!=phdr_seg0sz){
		fprintf(stderr,"seg0sz mismatch %08x %08x\n",shdr_seg0sz,phdr_seg0sz);
		exit(39);
	}
	if(shdr_segcsz && phdr_segcsz && shdr_segcsz!=phdr_segcsz){
		fprintf(stderr,"segcsz mismatch %08x %08x\n",shdr_segcsz,phdr_segcsz);
		exit(40);
	}
	if(shdr_seg0 && phdr_seg0 && shdr_seg0!=phdr_seg0){
		fprintf(stderr,"seg0 mismatch %08x %08x\n",shdr_seg0,phdr_seg0);
		exit(41);
	}
	if(shdr_segc && phdr_segc && shdr_segc!=phdr_segc){
		fprintf(stderr,"segc mismatch %08x %08x\n",shdr_segc,phdr_segc);
		exit(42);
	}
	if(shdr_c_start && phdr_c_start && shdr_c_start!=phdr_c_start){
		fprintf(stderr,"c_start mismatch %08x %08x\n",shdr_c_start,phdr_c_start);
		exit(43);
	}
	if(shdr_c_count>1 || phdr_c_count>1){
		fprintf(stderr,"c_count too high %08x %08x\n",shdr_c_count,phdr_c_count);
		exit(44);
	}
	seg0 = (unsigned char*)(map+shdr_seg0);
	segc = (unsigned char*)(map+shdr_segc);
	seg0sz = shdr_seg0sz;
	segcsz = shdr_segcsz;
	c_start = shdr_c_start;
	if(phdr_seg0)
		seg0 = (unsigned char*)(map+phdr_seg0);
	if(phdr_segc)
		segc = (unsigned char*)(map+phdr_segc);
	if(phdr_seg0sz)
		seg0sz = phdr_seg0sz;
	if(phdr_segcsz)
		segcsz = phdr_segcsz;
	if(phdr_c_start)
		c_start = phdr_c_start;
}

////////////////////////////////////////////////////////////////////////////////////////////////
static void mapit(char *name){
	int fd = open(name, O_RDONLY);
	if(fd<3){
		perror("open");
		exit(88);
	}
	struct stat sbuf;
	fstat(fd,&sbuf);
	char *map;
	map = mmap(0, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
	close(fd);
	file_start = map;

	if(!map || map==(char*)-1){
		fprintf(stderr,"%d is a bad size or mapping failed\n",(int)sbuf.st_size);
		exit(3);
	}
	int mapsize = sbuf.st_size;
	parse_elf(map,mapsize);
	fwrite(bindata,mkbin(),1,stdout);
}

////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[]){
	(void)argc;
	init_crc();
//  fprintf(stderr,"filename %s\n",argv[1]);
	mapit(argv[1]);
	return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
